1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 package com.sun.security.sasl.digest;
27
28 import java.security.AccessController;
29 import java.security.Provider;
30 import java.security.MessageDigest;
31 import java.security.NoSuchAlgorithmException;
32 import java.io.ByteArrayOutputStream;
33 import java.io.ByteArrayInputStream;
34 import java.io.IOException;
35 import java.io.UnsupportedEncodingException;
36 import java.util.Random;
37 import java.util.StringTokenizer;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.Arrays;
43
44 import java.util.logging.Logger;
45 import java.util.logging.Level;
46
47 import javax.security.sasl.*;
48 import javax.security.auth.callback.*;
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93 final class DigestMD5Server extends DigestMD5Base implements SaslServer {
94 private static final String MY_CLASS_NAME = DigestMD5Server.class.getName();
95
96 private static final String UTF8_DIRECTIVE = "charset=utf-8,";
97 private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess";
98
99
100
101
102
103 private static final int NONCE_COUNT_VALUE = 1;
104
105
106 private static final String UTF8_PROPERTY =
107 "com.sun.security.sasl.digest.utf8";
108
109
110 private static final String REALM_PROPERTY =
111 "com.sun.security.sasl.digest.realm";
112
113
114 private static final String[] DIRECTIVE_KEY = {
115 "username",
116 "realm",
117 "nonce",
118 "cnonce",
119 "nonce-count",
120 "qop",
121 "digest-uri",
122 "response",
123 "maxbuf",
124 "charset",
125 "cipher",
126 "authzid",
127 "auth-param",
128 };
129
130
131 private static final int USERNAME = 0;
132 private static final int REALM = 1;
133 private static final int NONCE = 2;
134 private static final int CNONCE = 3;
135 private static final int NONCE_COUNT = 4;
136 private static final int QOP = 5;
137 private static final int DIGEST_URI = 6;
138 private static final int RESPONSE = 7;
139 private static final int MAXBUF = 8;
140 private static final int CHARSET = 9;
141 private static final int CIPHER = 10;
142 private static final int AUTHZID = 11;
143 private static final int AUTH_PARAM = 12;
144
145
146 private String specifiedQops;
147 private byte[] myCiphers;
148 private List<String> serverRealms;
149
150 DigestMD5Server(String protocol, String serverName, Map props,
151 CallbackHandler cbh) throws SaslException {
152 super(props, MY_CLASS_NAME, 1, protocol + "/" + serverName, cbh);
153
154 serverRealms = new ArrayList<String>();
155
156 useUTF8 = true;
157
158 if (props != null) {
159 specifiedQops = (String) props.get(Sasl.QOP);
160 if ("false".equals((String) props.get(UTF8_PROPERTY))) {
161 useUTF8 = false;
162 logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1");
163 }
164
165 String realms = (String) props.get(REALM_PROPERTY);
166 if (realms != null) {
167 StringTokenizer parser = new StringTokenizer(realms, ", \t\n");
168 int tokenCount = parser.countTokens();
169 String token = null;
170 for (int i = 0; i < tokenCount; i++) {
171 token = parser.nextToken();
172 logger.log(Level.FINE, "DIGEST81:Server supports realm {0}",
173 token);
174 serverRealms.add(token);
175 }
176 }
177 }
178
179 encoding = (useUTF8 ? "UTF8" : "8859_1");
180
181
182 if (serverRealms.size() == 0) {
183 serverRealms.add(serverName);
184 }
185 }
186
187 public byte[] evaluateResponse(byte[] response) throws SaslException {
188 if (response.length > MAX_RESPONSE_LENGTH) {
189 throw new SaslException(
190 "DIGEST-MD5: Invalid digest response length. Got: " +
191 response.length + " Expected < " + MAX_RESPONSE_LENGTH);
192 }
193
194 byte[] challenge;
195 switch (step) {
196 case 1:
197 if (response.length != 0) {
198 throw new SaslException(
199 "DIGEST-MD5 must not have an initial response");
200 }
201
202
203 String supportedCiphers = null;
204 if ((allQop&PRIVACY_PROTECTION) != 0) {
205 myCiphers = getPlatformCiphers();
206 StringBuffer buf = new StringBuffer();
207
208
209
210 for (int i = 0; i < CIPHER_TOKENS.length; i++) {
211 if (myCiphers[i] != 0) {
212 if (buf.length() > 0) {
213 buf.append(',');
214 }
215 buf.append(CIPHER_TOKENS[i]);
216 }
217 }
218 supportedCiphers = buf.toString();
219 }
220
221 try {
222 challenge = generateChallenge(serverRealms, specifiedQops,
223 supportedCiphers);
224
225 step = 3;
226 return challenge;
227 } catch (UnsupportedEncodingException e) {
228 throw new SaslException(
229 "DIGEST-MD5: Error encoding challenge", e);
230 } catch (IOException e) {
231 throw new SaslException(
232 "DIGEST-MD5: Error generating challenge", e);
233 }
234
235
236
237 case 3:
238
239
240
241 try {
242 byte[][] responseVal = parseDirectives(response, DIRECTIVE_KEY,
243 null, REALM);
244 challenge = validateClientResponse(responseVal);
245 } catch (SaslException e) {
246 throw e;
247 } catch (UnsupportedEncodingException e) {
248 throw new SaslException(
249 "DIGEST-MD5: Error validating client response", e);
250 } finally {
251 step = 0;
252 }
253
254 completed = true;
255
256
257 if (integrity && privacy) {
258 secCtx = new DigestPrivacy(false );
259 } else if (integrity) {
260 secCtx = new DigestIntegrity(false );
261 }
262
263 return challenge;
264
265 default:
266
267 throw new SaslException("DIGEST-MD5: Server at illegal state");
268 }
269 }
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295 private byte[] generateChallenge(List<String> realms, String qopStr,
296 String cipherStr) throws UnsupportedEncodingException, IOException {
297 ByteArrayOutputStream out = new ByteArrayOutputStream();
298
299
300 for (int i = 0; realms != null && i < realms.size(); i++) {
301 out.write("realm=\"".getBytes(encoding));
302 writeQuotedStringValue(out, realms.get(i).getBytes(encoding));
303 out.write('"');
304 out.write(',');
305 }
306
307
308 out.write(("nonce=\"").getBytes(encoding));
309 nonce = generateNonce();
310 writeQuotedStringValue(out, nonce);
311 out.write('"');
312 out.write(',');
313
314
315
316 if (qopStr != null) {
317 out.write(("qop=\"").getBytes(encoding));
318
319 writeQuotedStringValue(out, qopStr.getBytes(encoding));
320 out.write('"');
321 out.write(',');
322 }
323
324
325 if (recvMaxBufSize != DEFAULT_MAXBUF) {
326 out.write(("maxbuf=\"" + recvMaxBufSize + "\",").getBytes(encoding));
327 }
328
329
330 if (useUTF8) {
331 out.write(UTF8_DIRECTIVE.getBytes(encoding));
332 }
333
334 if (cipherStr != null) {
335 out.write("cipher=\"".getBytes(encoding));
336
337 writeQuotedStringValue(out, cipherStr.getBytes(encoding));
338 out.write('"');
339 out.write(',');
340 }
341
342
343 out.write(ALGORITHM_DIRECTIVE.getBytes(encoding));
344
345 return out.toByteArray();
346 }
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386 private byte[] validateClientResponse(byte[][] responseVal)
387 throws SaslException, UnsupportedEncodingException {
388
389
390 if (responseVal[CHARSET] != null) {
391
392
393 if (!useUTF8 ||
394 !"utf-8".equals(new String(responseVal[CHARSET], encoding))) {
395 throw new SaslException("DIGEST-MD5: digest response format " +
396 "violation. Incompatible charset value: " +
397 new String(responseVal[CHARSET]));
398 }
399 }
400
401
402 int clntMaxBufSize =
403 (responseVal[MAXBUF] == null) ? DEFAULT_MAXBUF
404 : Integer.parseInt(new String(responseVal[MAXBUF], encoding));
405
406
407
408 sendMaxBufSize = ((sendMaxBufSize == 0) ? clntMaxBufSize :
409 Math.min(sendMaxBufSize, clntMaxBufSize));
410
411
412 String username;
413 if (responseVal[USERNAME] != null) {
414 username = new String(responseVal[USERNAME], encoding);
415 logger.log(Level.FINE, "DIGEST82:Username: {0}", username);
416 } else {
417 throw new SaslException("DIGEST-MD5: digest response format " +
418 "violation. Missing username.");
419 }
420
421
422 negotiatedRealm = ((responseVal[REALM] != null) ?
423 new String(responseVal[REALM], encoding) : "");
424 logger.log(Level.FINE, "DIGEST83:Client negotiated realm: {0}",
425 negotiatedRealm);
426
427 if (!serverRealms.contains(negotiatedRealm)) {
428
429
430 throw new SaslException("DIGEST-MD5: digest response format " +
431 "violation. Nonexistent realm: " + negotiatedRealm);
432 }
433
434
435
436 if (responseVal[NONCE] == null) {
437 throw new SaslException("DIGEST-MD5: digest response format " +
438 "violation. Missing nonce.");
439 }
440 byte[] nonceFromClient = responseVal[NONCE];
441 if (!Arrays.equals(nonceFromClient, nonce)) {
442 throw new SaslException("DIGEST-MD5: digest response format " +
443 "violation. Mismatched nonce.");
444 }
445
446
447 if (responseVal[CNONCE] == null) {
448 throw new SaslException("DIGEST-MD5: digest response format " +
449 "violation. Missing cnonce.");
450 }
451 byte[] cnonce = responseVal[CNONCE];
452
453
454 if (responseVal[NONCE_COUNT] != null &&
455 NONCE_COUNT_VALUE != Integer.parseInt(
456 new String(responseVal[NONCE_COUNT], encoding), 16)) {
457 throw new SaslException("DIGEST-MD5: digest response format " +
458 "violation. Nonce count does not match: " +
459 new String(responseVal[NONCE_COUNT]));
460 }
461
462
463 negotiatedQop = ((responseVal[QOP] != null) ?
464 new String(responseVal[QOP], encoding) : "auth");
465
466 logger.log(Level.FINE, "DIGEST84:Client negotiated qop: {0}",
467 negotiatedQop);
468
469
470 byte cQop;
471 if (negotiatedQop.equals("auth")) {
472 cQop = NO_PROTECTION;
473 } else if (negotiatedQop.equals("auth-int")) {
474 cQop = INTEGRITY_ONLY_PROTECTION;
475 integrity = true;
476 rawSendSize = sendMaxBufSize - 16;
477 } else if (negotiatedQop.equals("auth-conf")) {
478 cQop = PRIVACY_PROTECTION;
479 integrity = privacy = true;
480 rawSendSize = sendMaxBufSize - 26;
481 } else {
482 throw new SaslException("DIGEST-MD5: digest response format " +
483 "violation. Invalid QOP: " + negotiatedQop);
484 }
485 if ((cQop&allQop) == 0) {
486 throw new SaslException("DIGEST-MD5: server does not support " +
487 " qop: " + negotiatedQop);
488 }
489
490 if (privacy) {
491 negotiatedCipher = ((responseVal[CIPHER] != null) ?
492 new String(responseVal[CIPHER], encoding) : null);
493 if (negotiatedCipher == null) {
494 throw new SaslException("DIGEST-MD5: digest response format " +
495 "violation. No cipher specified.");
496 }
497
498 int foundCipher = -1;
499 logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}",
500 negotiatedCipher);
501
502
503 for (int j = 0; j < CIPHER_TOKENS.length; j++) {
504 if (negotiatedCipher.equals(CIPHER_TOKENS[j]) &&
505 myCiphers[j] != 0) {
506 foundCipher = j;
507 break;
508 }
509 }
510 if (foundCipher == -1) {
511 throw new SaslException("DIGEST-MD5: server does not " +
512 "support cipher: " + negotiatedCipher);
513 }
514
515 if ((CIPHER_MASKS[foundCipher]&HIGH_STRENGTH) != 0) {
516 negotiatedStrength = "high";
517 } else if ((CIPHER_MASKS[foundCipher]&MEDIUM_STRENGTH) != 0) {
518 negotiatedStrength = "medium";
519 } else {
520
521 negotiatedStrength = "low";
522 }
523
524 logger.log(Level.FINE, "DIGEST86:Negotiated strength: {0}",
525 negotiatedStrength);
526 }
527
528
529 String digestUriFromResponse = ((responseVal[DIGEST_URI]) != null ?
530 new String(responseVal[DIGEST_URI], encoding) : null);
531
532 if (digestUriFromResponse != null) {
533 logger.log(Level.FINE, "DIGEST87:digest URI: {0}",
534 digestUriFromResponse);
535 }
536
537
538
539
540
541
542
543
544
545 if (digestUri.equalsIgnoreCase(digestUriFromResponse)) {
546 digestUri = digestUriFromResponse;
547 } else {
548 throw new SaslException("DIGEST-MD5: digest response format " +
549 "violation. Mismatched URI: " + digestUriFromResponse +
550 "; expecting: " + digestUri);
551 }
552
553
554 byte[] responseFromClient = responseVal[RESPONSE];
555 if (responseFromClient == null) {
556 throw new SaslException("DIGEST-MD5: digest response format " +
557 " violation. Missing response.");
558 }
559
560
561 byte[] authzidBytes;
562 String authzidFromClient = ((authzidBytes=responseVal[AUTHZID]) != null?
563 new String(authzidBytes, encoding) : username);
564
565 if (authzidBytes != null) {
566 logger.log(Level.FINE, "DIGEST88:Authzid: {0}",
567 new String(authzidBytes));
568 }
569
570
571
572
573 char[] passwd;
574 try {
575
576 RealmCallback rcb = new RealmCallback("DIGEST-MD5 realm: ",
577 negotiatedRealm);
578 NameCallback ncb = new NameCallback("DIGEST-MD5 authentication ID: ",
579 username);
580
581
582 PasswordCallback pcb =
583 new PasswordCallback("DIGEST-MD5 password: ", false);
584
585 cbh.handle(new Callback[] {rcb, ncb, pcb});
586 passwd = pcb.getPassword();
587 pcb.clearPassword();
588
589 } catch (UnsupportedCallbackException e) {
590 throw new SaslException(
591 "DIGEST-MD5: Cannot perform callback to acquire password", e);
592
593 } catch (IOException e) {
594 throw new SaslException(
595 "DIGEST-MD5: IO error acquiring password", e);
596 }
597
598 if (passwd == null) {
599 throw new SaslException(
600 "DIGEST-MD5: cannot acquire password for " + username +
601 " in realm : " + negotiatedRealm);
602 }
603
604 try {
605
606 byte[] expectedResponse;
607
608 try {
609 expectedResponse = generateResponseValue("AUTHENTICATE",
610 digestUri, negotiatedQop, username, negotiatedRealm,
611 passwd, nonce ,
612 cnonce, NONCE_COUNT_VALUE, authzidBytes);
613
614 } catch (NoSuchAlgorithmException e) {
615 throw new SaslException(
616 "DIGEST-MD5: problem duplicating client response", e);
617 } catch (IOException e) {
618 throw new SaslException(
619 "DIGEST-MD5: problem duplicating client response", e);
620 }
621
622 if (!Arrays.equals(responseFromClient, expectedResponse)) {
623 throw new SaslException("DIGEST-MD5: digest response format " +
624 "violation. Mismatched response.");
625 }
626
627
628 try {
629 AuthorizeCallback acb =
630 new AuthorizeCallback(username, authzidFromClient);
631 cbh.handle(new Callback[]{acb});
632
633 if (acb.isAuthorized()) {
634 authzid = acb.getAuthorizedID();
635 } else {
636 throw new SaslException("DIGEST-MD5: " + username +
637 " is not authorized to act as " + authzidFromClient);
638 }
639 } catch (SaslException e) {
640 throw e;
641 } catch (UnsupportedCallbackException e) {
642 throw new SaslException(
643 "DIGEST-MD5: Cannot perform callback to check authzid", e);
644 } catch (IOException e) {
645 throw new SaslException(
646 "DIGEST-MD5: IO error checking authzid", e);
647 }
648
649 return generateResponseAuth(username, passwd, cnonce,
650 NONCE_COUNT_VALUE, authzidBytes);
651 } finally {
652
653 for (int i = 0; i < passwd.length; i++) {
654 passwd[i] = 0;
655 }
656 }
657 }
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673 private byte[] generateResponseAuth(String username, char[] passwd,
674 byte[] cnonce, int nonceCount, byte[] authzidBytes) throws SaslException {
675
676
677
678 try {
679 byte[] responseValue = generateResponseValue("",
680 digestUri, negotiatedQop, username, negotiatedRealm,
681 passwd, nonce, cnonce, nonceCount, authzidBytes);
682
683 byte[] challenge = new byte[responseValue.length + 8];
684 System.arraycopy("rspauth=".getBytes(encoding), 0, challenge, 0, 8);
685 System.arraycopy(responseValue, 0, challenge, 8,
686 responseValue.length );
687
688 return challenge;
689
690 } catch (NoSuchAlgorithmException e) {
691 throw new SaslException("DIGEST-MD5: problem generating response", e);
692 } catch (IOException e) {
693 throw new SaslException("DIGEST-MD5: problem generating response", e);
694 }
695 }
696
697 public String getAuthorizationID() {
698 if (completed) {
699 return authzid;
700 } else {
701 throw new IllegalStateException(
702 "DIGEST-MD5 server negotiation not complete");
703 }
704 }
705 }